feat: add AgentAdapter abstraction with Codex CLI support#95
Open
feat: add AgentAdapter abstraction with Codex CLI support#95
Conversation
132c6b4 to
d6a4cba
Compare
Replace hardcoded Claude Code transcript parsing with an extensible AgentAdapter trait. Each agent gets its own adapter for event mapping, file change extraction, transcript parsing, and token/model extraction. - AgentAdapter trait with ClaudeCode, Codex, and Default adapters - Codex transcript parsing: response_item, custom_tool_call, event_msg, apply_patch file changes from transcript chunks - CLI: protocol v2, repeatable --agent flag for init/stream - tracevault init --agent codex installs .codex/hooks.json - AgentBadge component with per-agent icon on session list/detail - Server uses AgentAdapterRegistry on AppState - Removes old hardcoded extract_file_change/is_file_modifying_tool Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d6a4cba to
555e6c2
Compare
…hooks and CLI flag Claude Code was rewritten in #95 instead of being ported from session_detail.rs::parse_record on main, which introduced display regressions: Bash/Glob toolUseResult lost their tool_name (wrong nested-key lookup), tool_result blocks lost their text body (read `text` instead of `content`), and assistant text formatting lost the \n\n separator and `[thinking] ` prefix. The parser is now a faithful port — same fields, same fallbacks, same format strings. Token extraction now mirrors main: presence of `usage` gates the whole RecordUsage and individual missing fields default to 0, instead of aborting on missing input/output_tokens. Codex adapter: - SessionStart matcher widened from "startup|resume" to "" so the hook also fires on /clear (verified against openai/codex sources). - The user-message system-prompt filter no longer drops every message starting with `<`. It now matches only the seven known Codex injection tags from codex protocol.rs (user_instructions, environment_context, apps_instructions, skills_instructions, plugins_instructions, collaboration_mode, realtime_conversation), preserving legitimate <div>/<svg>/<T>-style user questions. - File changes extracted from transcript chunks now use the chunk's own RFC 3339 timestamp (with fallback to the hook delivery time) rather than stamping every batched patch with the hook arrival time. CLI: - `tracevault init --agent <name>` is now additive: Claude Code hooks are always installed, additional --agent values are appended and deduplicated (with `claude` aliased to `claude-code`). Previously --agent codex replaced rather than augmented the default, so users following the README ended up without Claude hooks. - The success print now reflects which agents were actually installed instead of unconditionally claiming "Claude Code hooks installed". - README CLI table reworded to match the additive behavior. Cleanup: deduplicated adapter.is_file_modifying call in service/stream.rs (the result is already in `store_response`). Tests: 16 new adapter tests cover the regressed Claude Code parser paths (Bash/Glob/tool_result/thinking/system unknown subtype/progress edge cases) plus Codex token_usage edge cases and the Codex system-prompt whitelist. 5 new init tests cover the additive --agent behavior, dedup of `claude`/`claude-code` aliases, and the Codex SessionStart match-all matcher. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hic API
Pull adapter-specific knowledge out of `service/stream.rs`. Previously
the stream service hardcoded Codex chunk-shape lookups (`payload.name`,
`payload` cloning, RFC 3339 timestamp parsing) and used two extraction
methods with different return types (`Vec<ExtractedFileChange>` vs
`Vec<TranscriptFileChange>`), so the call site had to reach into chunk
internals to fill in tool_name / tool_input / timestamp.
The trait now exposes two symmetric methods returning the same
`FileChangeRecord` type:
fn file_changes_from_hook(&self, tool, input, ts) -> Vec<FileChangeRecord>
fn file_changes_from_transcript(&self, chunk, fallback_ts)
-> Vec<FileChangeRecord>
Each adapter overrides at most one. Defaults return empty. The
`FileChangeRecord` carries everything the persistence layer needs
(change, tool_name, tool_input, timestamp), so `stream.rs` just
iterates and inserts — no chunk shape knowledge anywhere outside the
adapter that owns that format.
Claude path is preserved bit-for-bit against main:
* `is_file_modifying` gate around the hook-extract loop is kept, so
Read/Glob/etc. skip the call entirely (matches main's
`if is_file_modifying_tool { ... }`).
* New `provides_transcript_file_changes()` capability flag (default
false) gates the per-line transcript-extract loop. Claude returns
false → the `file_changes_from_transcript` method is never invoked
for Claude transcript lines, exactly as on main where no equivalent
call existed.
* `file_changes_from_hook` for Claude wraps the same Write/Edit logic
that lived in `extract_file_change` on main; the resulting DB writes
have identical fields and timestamps (record.timestamp = req.timestamp).
CLI: replace the hardcoded `match agent.as_str() { "claude-code" => ...,
"codex" => ... }` in `main.rs` with `adapter.display_name()` and
`adapter.hooks_install_path()` from the trait, so adding a new agent
no longer requires touching the print-message code.
Codex: `file_changes_from_transcript` now resolves the chunk's RFC 3339
timestamp internally and returns it in each record, replacing the
duplicated timestamp logic that previously lived in `stream.rs`.
The `provides_transcript_file_changes` override is `true`.
Tests: 51 adapter tests (was 50), including a new fallback case
verifying that a chunk with no top-level timestamp falls back to the
hook delivery time. All hook/transcript extraction tests updated to
the new method names and return type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing --agent Move the "claude"/"claude-code" alias resolution and dedup off the CLI and onto the AgentAdapter::name() canonical id — the registry already maps both strings to the same adapter, so the manual match was redundant. Dedup now runs against the adapter's own id, not the user-provided string. Change semantics: --agent codex installs only Codex hooks. Claude Code is installed only when --agent is omitted entirely (default), instead of being appended unconditionally to every --agent invocation. .gitignore entries are derived from each installed adapter's hooks_install_path(), so a codex-only init no longer pins .claude/settings.json into the ignore list.
Multi-agent split caused subtle drift on the Claude code path. Restore parity with pre-multi-agent main: - wire_protocol_version() trait method (default v2); Claude overrides to v1 so request bytes match main - persists_model_without_usage() capability flag (default false); Codex sets it true. Server stream gate becomes has_tokens || (flag && model.is_some()), so Claude's update_tokens stays token-presence-only as in main - ClaudeCodeAdapter parser locks onto first tool_use block via seen_tool_use flag (matches main's arr.iter().find() semantics) - CLI stream uses adapter.wire_protocol_version() / adapter.name() for protocol_version + tool fields - init.rs installs hooks after .gitignore update (matches main order) Also: CLI init prints actually-installed gitignore entries instead of hardcoded paths, and a comment marks _event_type as unused (routing is via hook_event_name from stdin). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…E with --agent semantics
Codex Stop hook entry was missing the `matcher` field that all other
lifecycle hooks (SessionStart, PreToolUse, PostToolUse) already carry,
risking a silent no-op if Codex requires the field. README also still
described `--agent` as additive ("in addition to the Claude Code hooks")
even though the flag has been replacement-only since 6fad80f.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Replace hardcoded Claude Code transcript parsing with an extensible AgentAdapter trait and registry. Each AI coding agent gets its own adapter for event mapping, file change extraction, and transcript record parsing. Adds full Codex CLI support.
What's included
tracevault-coreresponse_item/message,custom_tool_call,event_msg,apply_patchfile changes from transcript chunks--agentflag forstreamcommand, Codex-compatible hook response formattracevault init --agent codex— installs Codex hooks in.codex/hooks.jsonextract_file_change/is_file_modifying_toolfromstreaming.rsHow it works
The server resolves the adapter from
sessions.toolcolumn (set by CLI via--agentflag). During ingestion (stream.rs), the adapter extracts tokens and file changes. During display (session_detail.rs,traces_ui.rs), it parses transcript chunks intoTranscriptRecords for the frontend.Codex file modifications come exclusively through transcript chunks (
custom_tool_callwithapply_patch), not through hook ToolUse events — the adapter handles this viaextract_file_changes_from_transcript.Checklist
cargo fmtpassescargo clippypasses🤖 Generated with Claude Code